/*
 * Colocar aqui a implementacao do algoritmo 
 * 
 * */
package example.loadbalance;

import java.util.ArrayList;

import br.edu.ufcg.copin.sd.Menssagem;
import br.edu.ufcg.copin.sd.TipoMenssagem;


import peersim.config.Configuration;
import peersim.config.FastConfig;
import peersim.core.*;
import peersim.vector.SingleValue;
import peersim.vector.SingleValueHolder;
import peersim.cdsim.CDProtocol;

/**
 * <p>
 * This class implements a (simple) load balancing strategy: each node selects
 * its most "distant" neighbor in terms of load difference and exchanges with it
 * an amount of load not exceeding the {@link #PAR_QUOTA} parameter.
 * </p>
 * <p>
 * The class subclasses {@link peersim.vector.SingleValueHolder} in order to be
 * type compatible with its observers and initializers object companions.
 * </p>
 * 
 */
public class RoutingBasedBalance extends BasicBalance implements CDProtocol {

	// ------------------------------------------------------------------------
	// Parameters
	// ------------------------------------------------------------------------

	/**
	 * Initial quota. Defaults to 1.
	 * 
	 * @config
	 */
	protected static final String PAR_QUOTA = "quota";

	/**
	 * Initial TARGET_NODE. Defaults to 3.
	 * 
	 * @config
	 */
	protected static final String TARGET_NODE = "target_node";

	// ------------------------------------------------------------------------
	// Fields
	// ------------------------------------------------------------------------

	/** Quota amount. Obtained from config property {@link #PAR_QUOTA}. */
	private final double quota_value;

	private final long target_node;

	protected double quota; // current cycle quota

	private Menssagem menssagem;

	private boolean hasNewMenssagem;

	// ------------------------------------------------------------------------
	// Initialization
	// ------------------------------------------------------------------------
	/**
	 * Standard constructor that reads the configuration parameters. Invoked by
	 * the simulation engine.
	 * 
	 * @param prefix
	 *            the configuration prefix for this class.
	 */
	public RoutingBasedBalance(String prefix) {
		super(prefix);
		// get quota value from the config file. Default 1.
		quota_value = (Configuration.getInt(prefix + "." + PAR_QUOTA, 1));
		target_node = (Configuration.getInt(prefix + "." + TARGET_NODE, 3));
		quota = quota_value;
	}

	// The clone() method is inherited.

	// ------------------------------------------------------------------------
	// Methods
	// ------------------------------------------------------------------------

	/** Resets the current node quota value. */
	protected void resetQuota() {
		this.quota = quota_value;
	}

	/**
	 * Using an underlying {@link Linkable} protocol choses a neighbor and
	 * performs a variance reduction step.
	 * 
	 * @param node the node on which this component is run.
	 * @param protocolID the id of this protocol in the protocol array.
	 */
	public void nextCycle(Node node, int protocolID) {
		int linkableID = FastConfig.getLinkable(protocolID);
		Linkable linkable = (Linkable) node.getProtocol(linkableID);
		SingleValue prot = (SingleValue) node.getProtocol(protocolID);
		double myLoad = prot.getValue();
		//Se chegou alguma menssagem

		//testando... se sou o no 0
		if(node.getIndex() == 1){
			menssagem = new Menssagem();
			ArrayList<Long> pathIda = new ArrayList<Long>();
			pathIda.add(node.getID());
			menssagem.setPathIda(pathIda);
			ArrayList<Double> loadIda = new ArrayList<Double>();
			loadIda.add(myLoad);
			menssagem.setLoadIda(loadIda);
			menssagem.setSessionId(System.currentTimeMillis());
			menssagem.setTargetNode(target_node);
			menssagem.setTipoDaMenssagem(TipoMenssagem.SETUP);
			sendSETUP(node,protocolID);
		}

		/*if(this.hasNewMenssagem){
			this.hasNewMenssagem = false;
			if(this.menssagem.getTipoDaMenssagem().equals(TipoMenssagem.SETUP)){

				//se eu nao sou o destino do exemplo mando SETUP senao mando um ACK
				if(this.menssagem.getTargetNode() != node.getID()){
					sendSETUP(node,protocolID);	
				}else{
					int statusDoErro = sendACK(node, protocolID, true);

					//se o ACK deu negativo, escolha outra rota pq essa ja falhou de cara
					if(statusDoErro < 0){
						//TODO Implementar
					}
				}

			}else if(this.getMenssagem().getTipoDaMenssagem().equals(TipoMenssagem.ACK)){

				//se eu nao sou o no source entao mando ACK pra frente senao trato o ACK
				if(node.getID() != 0)
					sendACK(node, protocolID, false);
				else{
					// TODO Implementar
				}

			}else if(this.getMenssagem().getTipoDaMenssagem().equals(TipoMenssagem.ERROR)){

				//se eu nao sou o destino do exemplo mando ERROR senao tenho que tratar o erro
				// e escolher uma nova rota
				if(this.menssagem.getTargetNode() != node.getID()){
					sendERROR(node,protocolID);	
				}else{
					//TODO Implementar
				}

			}else{
				//erro
			}
		}*/
	}

	//Metodo que envia uma menssagem de SETUP
	protected void sendSETUP(Node node, int protocolID){
		int linkableID = FastConfig.getLinkable(protocolID);
		Linkable linkable = (Linkable) node.getProtocol(linkableID);
		
		System.out.println("O no de origem eh o : "+node.getIndex());
		System.out.println("O no de origem tem : "+linkable.degree()+" vizinhos");

		//valor atual de carga
		SingleValue prot = (SingleValue) node.getProtocol(protocolID);
		double myLoad = prot.getValue();

		//Adiciona os vizinhos da origem
		ArrayList<Integer> vizinhos = new ArrayList<Integer>();

		ArrayList<Integer> aux = new ArrayList<Integer>();

		//A primeira rodada tem q ser fora do while
		//laco que percorre todos os vizinhos e manda um SETUP
		for (int i = 0; i < linkable.degree(); ++i) {
			System.out.println("1");
			//vizinhos da instancia atual do RoutingBasedBalance
			Node peer = linkable.getNeighbor(i);
			System.out.println("2");
			//Adiciona os promeiros vizinhos do no origem
			vizinhos.add(peer.getIndex());
			System.out.println("3");
			System.out.println("eu, indice="+peer.getIndex()+" sou vizinho de primeira ordem do no: "+node.getIndex());
			
			//Se o peer morreu ignore
			if (!peer.isUp()){
				System.out.println("4");
				System.out.println("no "+peer.getIndex()+" esta morto");
				continue;
			}

			RoutingBasedBalance destino = (RoutingBasedBalance) peer.getProtocol(protocolID);
			System.out.println("5");
			
			//Informa ao vizinho que uma menssagem ira chegar
			destino.setHasNewMenssagem(true);
			System.out.println("6");

			//seta a menssagem no vizinho para ser capturada no metodo nextCycle dele
			destino.setMenssagem(menssagem);
			System.out.println("no de origem manda msg para no : "+peer.getIndex());
		}


		//Enquanto nao tiver mais nos execute
		while(!vizinhos.isEmpty()){
			
			System.out.println("o array vizinhos tem os seguintes nos:");
			for(int j = 0; j < vizinhos.size(); j++){
				Node peer0 = Network.get( vizinhos.get(j) );
				System.out.println(peer0.getIndex()+" - ");
			}

			//Manda SETUP para todos da lista
			for(int j = 0; j < vizinhos.size(); j++){
				Node peer0 = Network.get( vizinhos.get(j) );
				//int pid = peer.protocolSize() -1;   ISSO EH IGUAL AO protocolID
				linkableID = FastConfig.getLinkable(protocolID);
				linkable = (Linkable) peer0.getProtocol(linkableID);

				System.out.println("eu, no="+peer0.getIndex()+" tenho "+linkable.degree()+" vizinhos");
				System.out.println("hora do no "+peer0.getIndex()+" comecar a mandar SETUPs para seus vizinhos");

				//laco que percorre todos os vizinhos e manda um SETUP para o no [j] da lista
				for (int i = 0; i < linkable.degree(); ++i) {
					
					//vizinhos da instancia atual do RoutingBasedBalance
					Node peer = linkable.getNeighbor(i);
					
					//valor atual de carga
					prot = (SingleValue) peer.getProtocol(protocolID);
					myLoad = prot.getValue();

					System.out.println("eu, o no "+peer0.getIndex()+" vou mandar um SETUP para meu vizinho "+peer.getIndex());
					
					//Se o peer morreu ignore
					if (!peer.isUp()){
						System.out.println("no "+peer.getIndex()+" esta morto");
						continue;
					}

					RoutingBasedBalance destino = (RoutingBasedBalance) peer.getProtocol(protocolID);

					//seta a menssagem de SETUP
					Menssagem msg = preencheMenssagem(peer.getID(),myLoad,TipoMenssagem.SETUP);

					//Se o meu vizinho ja recebeu uma menssagem verifique
					if(destino.getMenssagem() != null){
						System.out.println("O no "+peer.getIndex()+" ja tem uma menssagem");
						if(destino.getMenssagem().getSessionId() != msg.getSessionId()){
							//Informa ao vizinho que uma menssagem ira chegar
							destino.setHasNewMenssagem(true);

							//seta a menssagem no vizinho para ser capturada no metodo nextCycle dele
							destino.setMenssagem(msg);

							//adiciona o index dos vizinhos nesta lista auxiliar para depois ser adicionada na lista de vizinhos
							//ja adiciona na ordem normal para ser lida posteriormente da mesma forma
							aux.add(peer.getIndex());

							System.out.println("o no "+peer.getIndex()+" acabou de receber uma menssagem");
						}
					//Se o meu vizinho ainda nao recebeu esta menssagem especifica, envie o SETUP
					}else{
						//Informa ao vizinho que uma menssagem ira chegar
						destino.setHasNewMenssagem(true);

						//seta a menssagem no vizinho para ser capturada no metodo nextCycle dele
						destino.setMenssagem(msg);

						//adiciona o index dos vizinhos nesta lista auxiliar para depois ser adicionada na lista de vizinhos
						//ja adiciona na ordem normal para ser lida posteriormente da mesma forma
						aux.add(peer.getIndex());

						System.out.println("o no "+peer.getIndex()+" acabou de receber uma menssagem");
					}
				}

			}
			vizinhos.removeAll(vizinhos);
			if(!vizinhos.isEmpty()){
				vizinhos.addAll(aux);
				aux.removeAll(aux);
			}else{
				;//ERRO
			}

			/*
			//laco que percorre todos os vizinhos e manda um SETUP
			for (int i = 0; i < linkable.degree(); ++i) {
				//vizinhos da instancia atual do RoutingBasedBalance
				Node peer = linkable.getNeighbor(i);

				//Se o peer morreu ignore
				if (!peer.isUp())
					continue;

				RoutingBasedBalance destino = (RoutingBasedBalance) peer.getProtocol(protocolID);

				//seta a menssagem de SETUP
				Menssagem msg = preencheMenssagem(node.getID(),myLoad,TipoMenssagem.SETUP);

				//Se o meu vizinho ainda nao recebeu esta menssagem especifica envie o SETUP
				if(destino.getMenssagem() != null && destino.getMenssagem().getSessionId() != msg.getSessionId()){

					//Informa ao vizinho que uma menssagem ira chegar
					destino.setHasNewMenssagem(true);

					//seta a menssagem no vizinho para ser capturada no metodo nextCycle dele
					destino.setMenssagem(msg);
				}//senao continue;
			}*/
		}
	}

	//Metodo que envia uma menssagem de ACK
	protected int sendACK(Node node, int protocolID, boolean isTarget){
		//pega o indice do no atual no arraylist de pathIda da menssagem
		Integer index = menssagem.getPathIda().lastIndexOf(node.getID());

		//pega o ID do proximo no a receber o ACK: index-1 eh no minimo 0
		long nextNoID = menssagem.getPathIda().get( (index-1) );

		//ATENCAO: VER SE EXISTE UMA FORMA DE ACESSO DIRETO AO NO!!!
		//ESTA FORMA VARRE TODA A REDE!!!
		for (int i = 0; i < Network.size(); i++) {
			Node nextPeer = Network.get(i);
			if(nextPeer.getID() == nextNoID){
				//se o proximo no estiver vivo entao mande o ACK
				if (nextPeer.isUp()){

					//mude somente o tipo da menssagem para ACK o resto permanece o mesmo
					Menssagem msg = preencheMenssagem(-1,-1,TipoMenssagem.ACK);
					RoutingBasedBalance destino = (RoutingBasedBalance) nextPeer.getProtocol(protocolID);

					//Informa ao vizinho que uma menssagem ira chegar
					destino.setHasNewMenssagem(true);

					//seta a menssagem no vizinho para ser capturada no metodo nextCycle dele
					destino.setMenssagem(msg);
					break;
					//Se o proximo peer morreu, descarta o ACK e entao retorna uma menssagem de ERRO para o target
				}else{
					if(!isTarget){
						//se o no atual nao for o no de destino
						sendERROR(node, protocolID, node.getIndex());
					}else{
						//se o no atual for o no de  destino retorne -1 indicando para escolher nova rota
						return -1;
					}
					break;
				}
			}
		}
		return 1;
	}

	//Metodo que envia uma menssagem de ERROR
	protected void sendERROR(Node node, int protocolID, int tempIndex){
		Menssagem msg = preencheMenssagem(-1,-1,TipoMenssagem.ERROR);
		Node backPeer = Network.get(tempIndex);
		if (backPeer.isUp()){
			RoutingBasedBalance destino = (RoutingBasedBalance) backPeer.getProtocol(protocolID);
			//Informa ao vizinho que uma menssagem ira chegar
			destino.setHasNewMenssagem(true);

			//seta a menssagem no vizinho para ser capturada no metodo nextCycle dele
			destino.setMenssagem(msg);
		}
	}

	//Metodo que envia uma menssagem de ERROR
	protected void sendERROR(Node node, int protocolID){

		//pega o indice do no atual no arraylist de pathIda da menssagem
		Integer index = menssagem.getPathIda().lastIndexOf(node.getID());

		//pega o ID do proximo no a receber o ERROR: index+1 eh no max o tamanho do array
		long nextNoID = menssagem.getPathIda().get( (index+1) );

		//ATENCAO: VER SE EXISTE UMA FORMA DE ACESSO DIRETO AO NO!!!
		//ESTA FORMA VARRE TODA A REDE!!!
		for (int i = 0; i < Network.size(); i++) {
			Node backPeer = Network.get(i);
			if(backPeer.getID() == nextNoID){			

				//se o proximo no estiver vivo entao mande o ERROR
				if (backPeer.isUp()){
					Menssagem msg = preencheMenssagem(-1,-1,TipoMenssagem.ERROR);

					//mude somente o tipo da menssagem para ERROR o resto permanece o mesmo
					RoutingBasedBalance destino = (RoutingBasedBalance) backPeer.getProtocol(protocolID);

					//Informa ao vizinho que uma menssagem ira chegar
					destino.setHasNewMenssagem(true);

					//seta a menssagem no vizinho para ser capturada no metodo nextCycle dele
					destino.setMenssagem(msg);
				}
			}
		}
	}

	private Menssagem preencheMenssagem(long IDdoNoAtual, double carga, TipoMenssagem tipoDaMenssagem){
		Menssagem mens = null;
		
		if (this.menssagem != null){
			//copia a menssagem atual com todo o path e demais propriedades
			try {
				mens = menssagem.clone();
			} catch (CloneNotSupportedException e) {
				e.printStackTrace();
			}
			
			//adiciona novas informacoes na menssagem: ID do no
			if( IDdoNoAtual >0 )
				mens.getPathIda().add(IDdoNoAtual);

			//adiciona novas informacoes na menssagem: valor da carga do no
			if( carga >0 )
				mens.getLoadIda().add(carga);

			//informa o tipo de menssagem: SETUP, ACK, ERROR
			mens.setTipoDaMenssagem(tipoDaMenssagem);
		}else{
			mens = new Menssagem();
			ArrayList<Long> pathIda = new ArrayList<Long>();
			pathIda.add(IDdoNoAtual);
			mens.setPathIda(pathIda);
			ArrayList<Double> loadIda = new ArrayList<Double>();
			loadIda.add(carga);
			mens.setLoadIda(loadIda);
			mens.setSessionId(System.currentTimeMillis());
			mens.setTargetNode(target_node);
			mens.setTipoDaMenssagem(tipoDaMenssagem);
		}
		//return
		return mens;
	}

	/**
	 * Performs the actual load exchange selecting to make a PUSH or PULL
	 * approach. It affects the involved nodes quota.
	 * 
	 * @param neighbor
	 *            the selected node to talk with. It is assumed that it is an
	 *            instance of the {@link example.loadbalance.BasicBalance}
	 *            class.
	 * @throws CloneNotSupportedException 
	 */	
	protected void doTransfer(RoutingBasedBalance neighbor){
		double a1 = this.getValue();
		double a2 = neighbor.getValue();
		double maxTrans = Math.abs((a1 - a2) / 2);
		double trans = Math.min(maxTrans, quota);
		trans = Math.min(trans, neighbor.quota);
		if (a1 <= a2) // PULL phase - aumentar CARGA local
		{
			a1 += trans;
			a2 -= trans;
		} else // PUSH phase - diminuir CARGA local
		{
			a1 -= trans;
			a2 += trans;
		}
		this.setValue(a1);
		this.quota -= trans; //diminuindo COTA DE TRANSFERÊNCIA
		neighbor.setValue(a2);
		neighbor.quota -= trans;
	}

	public Menssagem getMenssagem() {
		return menssagem;
	}

	public void setMenssagem(Menssagem menssagem) {
		this.menssagem = menssagem;
	}

	public boolean isHasNewMenssagem() {
		return hasNewMenssagem;
	}

	public void setHasNewMenssagem(boolean hasNewMenssagem) {
		this.hasNewMenssagem = hasNewMenssagem;
	}

}
